package dkbta.ontology.loading;

import static dkbta.util.Utils.enumForName;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import dkbta.ontology.Ontology;
import dkbta.ontology.elements.ElementType;
import dkbta.ontology.elements.pattern.conditions.DurationCondition;
import dkbta.ontology.values.AttributeValueCondition;
import dkbta.ontology.values.DurationValues;
import dkbta.ontology.values.ElementValueCondition;
import dkbta.ontology.values.NumericValues;
import dkbta.ontology.values.PercentValues;
import dkbta.ontology.values.ScaledNumericValues;
import dkbta.ontology.values.ScaledSymbolicValues;
import dkbta.ontology.values.ScaledValues;
import dkbta.ontology.values.SymbolicValues;
import dkbta.ontology.values.ValueCondition;
import dkbta.ontology.values.Values;
import dkbta.util.Log;
import dkbta.util.Utils;
import dkbta.util.time.TimeUnit;

public class OntologyParser{
	private final Log _log;

	private Ontology _ontology;

	public OntologyParser(){
		_log = Log.getLogger();
	}

	public Ontology load(File ontologyFile) throws Exception{
		Element ontology = Utils.readDOMDocFromXML(ontologyFile).getDocumentElement();

		// Initializing the ontology
		_ontology = new Ontology(ontology.getAttribute("name"));

		// Parsing all of the ontology's elements
		parseElements(ontology, new PrimitiveParser(_ontology));
		parseElements(ontology, new EventParser(_ontology));
		parseElements(ontology, new ContextParser(_ontology));
		parseElements(ontology, new StateParser(_ontology));
		parseElements(ontology, new GradientParser(_ontology));
		parseElements(ontology, new RateParser(_ontology));
		parseElements(ontology, new TrendParser(_ontology));
		parseElements(ontology, new PatternParser(_ontology));
		parseElements(ontology, new RepeatingPatternParser(_ontology));
		parseElements(ontology, new SubjectStatParser(_ontology));
		parseElements(ontology, new StatParser(_ontology));

		// Checking that all of the ontology's elements are properly interconnected
		_ontology.validateOntology();

		return _ontology;
	}

	private void parseElements(Element ontology, ElementParser ep)
			throws OntologyException{
		ElementType elementType = ep.getElementType();
		String nameInSingle = elementType.toString();
		String nameInPlural = nameInSingle + "s";
		Element containingElement = OntologyParser.getXmlElement(ontology, nameInPlural);
		if (containingElement == null){
			return; // No elements of the type are defined in the ontology
		}

		// Parsing each of the elements
		NodeList childNodes = containingElement.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; ++i){
			Node node = childNodes.item(i);
			if (node.getNodeType() != Node.ELEMENT_NODE
					|| !node.getNodeName().equalsIgnoreCase(nameInSingle)){
				continue;
			}
			Element element = (Element)childNodes.item(i);

			// Parsing the element's name and component type
			String componentType = element.getAttribute("componentType");
			String name = element.getAttribute("name");
			if (Utils.isEmpty(name)){
				_log.warning(nameInSingle + " without a name attribute");
				continue;
			}
			try{
				// Parsing the rest of the fields
				ep.parseElement(element, name, componentType);

			}catch(Exception e){
				_log.warning("Unable to parse " + nameInSingle + ": " + name, e);
			}
		}
	}

	static DurationCondition parseDurationCondition(Element ontElement)
			throws OntologyException{
		Element dcElement = getXmlElement(ontElement, "DurationCondition");
		if (dcElement == null){
			return null;
		}
	
		return new DurationCondition(parseDurationValues(dcElement, "min", "max"));
	}

	static DurationValues parseDurationValues(Element dvElement, String minName,
		String maxName) throws OntologyException{
		String minE = dvElement.getAttribute(minName + "E");
		String min = dvElement.getAttribute(minName);
		String maxE = dvElement.getAttribute(maxName + "E");
		String max = dvElement.getAttribute(maxName);
	
		return new DurationValues(minE, min, maxE, max);
	}

	/**
	 * Returns the single element with the tagName with the given parent element
	 * 
	 * @param parent The element containing the element with the requested tag name
	 * @param tagName The tag name
	 * @return The requested element if only a single such element exists, null if no such element exists
	 * @throws OntologyException If multiple elements with the tag exist
	 */
	public static Element getXmlElement(Element parent, String tagName)
			throws OntologyException{
		NodeList nodes = parent.getElementsByTagName(tagName);
		if (nodes.getLength() == 0){
			return null;
		}
		if (nodes.getLength() != 1){
			throw new OntologyException("Multile '" + tagName + "' elements");
		}

		return (Element)nodes.item(0);
	}

	static ValueCondition parseValueConsdition(Element element, String tag,
		Set<ElementType> invalidTypes, boolean allowEventsWithoutAttributes)
			throws OntologyException{
		ElementType et = enumForName(ElementType.class, tag);
		if (invalidTypes.contains(et)){
			throw new OntologyException("Invalid element: " + et);
		}
		String elementName = element.getAttribute("name");
		if (Utils.isEmpty(elementName)){
			throw new OntologyException("Invalid element name: " + elementName);
		}

		Values values = parseValues(element);
		if (et == ElementType.EVENT){
			String attributeName = element.getAttribute("attributeName");
			if (Utils.isEmpty(attributeName)){
				if (allowEventsWithoutAttributes){
					if (values != null){
						throw new OntologyException(
								"You cannot define a value constraint on the value of an event");
					}
					return new ElementValueCondition(elementName, et, null);
				}else{
					throw new OntologyException("Invalid event attribute name: "
							+ attributeName);
				}
			}else{
				return values == null ? null : new AttributeValueCondition(elementName,
						attributeName, values);
			}
		}else{
			return values == null ? null : new ElementValueCondition(elementName, et,
					values);
		}
	}

	static ScaledValues parseScaledValues(Element parent) throws OntologyException{
		ScaledValues sv = parseScaledNumericValues(parent);
		if (sv == null){
			sv = parseScaledSymbolicValues(parent, true);
		}
		return sv;
	}

	static Values parseValues(Element parent) throws OntologyException{
		Values v = parseNumericValues(parent, true, null);
		if (v == null){
			v = parseSymbolicValues(parent, true);
		}
		return v;
	}

	static ScaledSymbolicValues parseScaledSymbolicValues(Element element,
		boolean isParent) throws OntologyException{
		Element e = isParent ? OntologyParser.getXmlElement(element, "SymbolicValues")
				: element;
		if (e == null){
			return null;
		}
		String scale = e.getAttribute("scale");
		SymbolicValues values = parseSymbolicValues(element, isParent);

		return new ScaledSymbolicValues(scale, values);
	}

	static SymbolicValues parseSymbolicValues(Element element, boolean isParent)
			throws OntologyException{
		Element e = isParent ? OntologyParser.getXmlElement(element, "SymbolicValues")
				: element;
		if (e == null){
			return null;
		}

		NodeList valueNodes = e.getElementsByTagName("Value");
		List<String> values = new ArrayList<String>(valueNodes.getLength());
		for (int i = 0; i < valueNodes.getLength(); ++i){
			Element valueElement = (Element)valueNodes.item(i);
			String value = valueElement.getAttribute("name");
			if (value.isEmpty()){
				throw new OntologyException("Value without a name");
			}
			values.add(value);
		}

		return new SymbolicValues(values);
	}

	static ScaledNumericValues parseScaledNumericValues(Element parent)
			throws OntologyException{
		Element e = OntologyParser.getXmlElement(parent, "NumericValues");
		if (e == null){
			return null;
		}

		String scale = e.getAttribute("scale");
		NumericValues values = parseNumericValues(parent, true, null);
		String unit = e.getAttribute("unit");

		return new ScaledNumericValues(scale, values, unit);
	}

	static NumericValues parseNumericValues(Element element, boolean isParent,
		TimeUnit timeUnit) throws OntologyException{
		Element e = isParent ? OntologyParser.getXmlElement(element, "NumericValues")
				: element;
		if (e == null){
			return null;
		}

		String minE = e.getAttribute("minE");
		String min = e.getAttribute("min");
		String maxE = e.getAttribute("maxE");
		String max = e.getAttribute("max");

		return new NumericValues(minE, min, maxE, max, timeUnit);
	}

	static PercentValues parsePercentRange(Element element, boolean isParent,
		TimeUnit timeUnit) throws OntologyException{
		Element e = isParent ? OntologyParser.getXmlElement(element, "PercentRange")
				: element;
		if (e == null){
			return null;
		}

		String minE = e.getAttribute("minE");
		String min = e.getAttribute("min");
		String maxE = e.getAttribute("maxE");
		String max = e.getAttribute("max");

		return new PercentValues(minE, min, maxE, max, timeUnit);
	}

	interface ElementParser{
		public void parseElement(Element element, String name, String componentType)
				throws OntologyException;

		public ElementType getElementType();
	}
}
